package io.elegant.sessions.media

import android.Manifest
import android.content.Context
import android.media.MediaRecorder
import android.os.Build
import androidx.annotation.RequiresApi
import com.google.protobuf.Any
import io.elegant.*
import io.elegant.ElegantApiGrpc.ElegantApiStub
import io.elegant.sessions.BaseSession
import io.grpc.stub.StreamObserver
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import java.io.File
import java.lang.IllegalStateException
import java.util.*

class MediaRecorderSession(
    stub: ElegantApiStub,
    private val context: Context
) : BaseSession(stub, context) {
    private var formatAndEncoding = 0 to 0
    private var recordedListener: ((File?) -> Unit)? = null
    override val sendStreamObserver: StreamObserver<Elegant.Interaction> = stub.mediaRecorder(receiveStreamObserver)
    private var recorder: MediaRecorder? = null
    private var client: OkHttpClient? = null
    private var file: File? = null
    private var errorCallbacks: MutableSet<(Int, Int) -> Unit>? = null
    private fun catchMediaRecorderError(sessionId: Long, futureId: Long) {
        fun cb(i: Int, i2: Int) {
            Exception("An error occurred in the media recorder, and the error codes are ($i, $i2).")
                .sendError(sessionId, futureId)
            errorCallbacks?.remove(::cb)
        }
        errorCallbacks?.add (::cb)
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    @Suppress("DEPRECATION")
    override fun onCreate(sessionId: Long, futureId: Long, vararg argv: Any) {
        val format = argv
                .takeIf { it.isNotEmpty() }
                ?.first()
                ?.toStringValue()
        file = File(context.cacheDir, "/record_${Date().time}.$format")
        context
            .toActivity()
            ?.requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO)) {}
        formatAndEncoding = when (format) {
            "3gp" -> MediaRecorder.OutputFormat.THREE_GPP to MediaRecorder.AudioEncoder.AMR_NB
            "ogg" -> MediaRecorder.OutputFormat.OGG to MediaRecorder.AudioEncoder.OPUS
            "aac" -> MediaRecorder.OutputFormat.AAC_ADTS to MediaRecorder.AudioEncoder.AAC
            else -> throw Exception("Format Error")
        }
        recorder = MediaRecorder()
        recorder?.setOnErrorListener { _, i, i2 ->
            errorCallbacks?.forEach {
                it(i, i2)
            }
        }
        client = OkHttpClient
            .Builder()
            .build()
        super.onCreate(sessionId, futureId, *argv)
    }

    override fun onDestroy() {
        recorder?.release()
        recorder = null
        client = null
        file
            ?.takeIf { it.exists() }
            ?.delete()
        errorCallbacks?.clear()
        errorCallbacks = null
        super.onDestroy()
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override suspend fun onReceive(name: String, sessionId: Long, futureId: Long, vararg argv: Any) {
        when (name) {
            "start" -> start(sessionId, futureId)
            "stop" -> stop(sessionId, futureId)
            "pause" -> pause()
            "resume" -> resume()
            "getFilename" -> getFilename(sessionId, futureId)
            "upload" -> upload(sessionId, futureId, *argv)
            else -> super.onReceive(name, sessionId, futureId, *argv)
        }
    }

    @RequiresApi(Build.VERSION_CODES.Q)
    private fun start(sessionId: Long, futureId: Long) {
        file
            ?.takeIf { it.exists() }
            ?.delete()
        catchMediaRecorderError(sessionId, futureId)
        try {
            recorder?.apply {
                reset()
                setAudioSource(MediaRecorder.AudioSource.MIC)
                setOutputFormat(formatAndEncoding.first)
                setOutputFile(file)
                setAudioEncoder(formatAndEncoding.second)
                prepare()
                start()
            }
        } catch (e: Exception) {
            e.sendError(sessionId, futureId)
        }
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    private fun stop(sessionId: Long, futureId: Long) {
        catchMediaRecorderError(sessionId, futureId)
        try {
            recorder?.let {
                it.resume()
                it.stop()
            }
        } catch (e: IllegalStateException) {
            e.sendError(sessionId, futureId)
        }
        csMain.launch {
            delay(500)
            recordedListener?.let { it(file) }
            onStop(sessionId, futureId, true.toAny())
        }
    }
    private fun onStop(sessionId: Long, futureId: Long, result: Any) = sendAction("onStop", sessionId, futureId, result)
    private fun upload(sessionId: Long, futureId: Long, vararg argv: Any) {
        val url = argv[0].toStringValue()
        val stream = file
            ?.inputStream()
            ?: throw Exception("There are no files to upload.")
        client?.upload(this, csMain, argv[1], stream, "audio/*", file?.name, url) {
            if (it == null) onUpload(sessionId, futureId, true.toAny())
            else it.sendError(sessionId, futureId)
        }
    }
    private fun onUpload(sessionId: Long, futureId: Long, result: Any) = sendAction("onUpload", sessionId, futureId, result)
    fun setOnRecorded(callback: (File?) -> Unit) {
        recordedListener = callback
    }
    private fun pause() = recorder?.pause()
    private fun resume() = recorder?.resume()
    private fun getFilename(sessionId: Long, futureId: Long) {
        val filename = file?.name.toAny()
        onGetFilename(sessionId, futureId, filename)
    }
    private fun onGetFilename(sessionId: Long, futureId: Long, filename: Any) = sendAction("onGetFilename", sessionId, futureId, filename)
}
